/*
 * Copyright (c) 2007 The Trustees of Indiana University. All rights reserved.
 * IU CSCI B534 Distributed Systems 2007-02-01 http://www.cs.indiana.edu/classes/b534
 */

package edu.indiana.cs.b534.torrent.impl;

import static edu.indiana.cs.b534.torrent.MessageType.BitField;
import static edu.indiana.cs.b534.torrent.MessageType.Cancel;
import static edu.indiana.cs.b534.torrent.MessageType.Choke;
import static edu.indiana.cs.b534.torrent.MessageType.Have;
import static edu.indiana.cs.b534.torrent.MessageType.Interested;
import static edu.indiana.cs.b534.torrent.MessageType.NotInterested;
import static edu.indiana.cs.b534.torrent.MessageType.Piece;
import static edu.indiana.cs.b534.torrent.MessageType.Request;
import static edu.indiana.cs.b534.torrent.MessageType.UnChoke;

import java.nio.ByteBuffer;

import edu.indiana.cs.b534.torrent.MessageType;
import edu.indiana.cs.b534.torrent.TStruct;
import edu.indiana.cs.b534.torrent.TorrentException;
import edu.indiana.cs.b534.torrent.Utils;

public class MessageUtil {

	public static final int HANDSHAKE_SIZE = 68;
	
    private byte[] infoHash;
    private String myPeerId; // SHA-1 checksum of torrent file and unique id of me
    private String connectingPeerID;
    private static final String PROTOCOL_HEADER = "BitTorrent protocol";
    private static final ByteBuffer PROTOCOL_HEADER_BYTES = TStruct.CHARSET.encode(PROTOCOL_HEADER);
    private static final byte[] RESERVED_HEADER_BYTES = new byte[]{0, 0, 0, 0, 0, 0, 0, 0};

    private int capacity;


    /**
     * This will be called when we get a request from another peer.
     *
     * @param handshake
     */
    public MessageUtil(byte[] handshake, byte[] infoHash,String myPeerID) throws TorrentException {
        isEqual(PROTOCOL_HEADER_BYTES.array(), handshake, 0);
        //TODO make sure this works
        this.infoHash = infoHash;
        this.myPeerId = myPeerID;
        this.connectingPeerID = new String(handshake, 48, 20);

        byte[] receivedInfoHash = new byte[20];
        System.arraycopy(handshake, 28, receivedInfoHash, 0, 20);

        if (!Utils.compareByteArrays(infoHash, receivedInfoHash)) {
            throw new TorrentException("Received info hash from peer " + myPeerId + " is different from the expected infohash. Closing the connection");
        }
        //this.infoHash = new String(handshake,28,20);

        capacity =
                PROTOCOL_HEADER.length() + 1 +
                        RESERVED_HEADER_BYTES.length +
                        infoHash.length +
                        myPeerId.getBytes().length;
    }


    /**
     * This is when we are trying to connect to another peer
     *
     * @param infoHash
     * @param myPeerId
     */
    public MessageUtil(byte[] infoHash, String myPeerId) {
        this.infoHash = infoHash;
        this.myPeerId = myPeerId;

        capacity =
                PROTOCOL_HEADER.length() +  1 +
                        RESERVED_HEADER_BYTES.length +
                        infoHash.length +
                        myPeerId.getBytes().length;
    }
    
    
    public void veirfyIncomingHandshake(byte[] handshake) throws TorrentException{
    	this.connectingPeerID = new String(handshake, 48, 20);

        byte[] receivedInfoHash = new byte[20];
        System.arraycopy(handshake, 28, receivedInfoHash, 0, 20);

        if (!Utils.compareByteArrays(infoHash, receivedInfoHash)) {
            throw new TorrentException("Received info hash from peer " + myPeerId + " is different from the expected infohash. Closing the connection");
        }
    }

    private byte[] buildMessage(MessageType type, byte[] payload) {

        ByteBuffer bb = ByteBuffer.allocate(payload.length + 1);
        byte msgType = 0;

        if (MessageType.Choke.equals(type)) {
            msgType = 0;
        } else if (MessageType.UnChoke.equals(type)) {
            msgType = 1;
        } else if (MessageType.Interested.equals(type)) {
            msgType = 2;
        } else if (MessageType.NotInterested.equals(type)) {
            msgType = 3;
        } else if (MessageType.Have.equals(type)) {
            msgType = 4;
        } else if (MessageType.BitField.equals(type)) {
            msgType = 5;
        } else if (MessageType.Request.equals(type)) {
            msgType = 6;
        } else if (MessageType.Piece.equals(type)) {
            msgType = 7;
        } else if (MessageType.Cancel.equals(type)) {
            msgType = 8;
        } else {
            throw new RuntimeException("Uknown message Type");
        }
        bb.put(msgType);
        bb.put(payload);
        return bb.array();
    }

    private byte[] buildMessage(MessageType type) {
        return buildMessage(type, new byte[]{});
    }

    public byte[] buildChoke() {
        return buildMessage(Choke);
    }

    public byte[] buildUnChoke() {
        return buildMessage(UnChoke);
    }

    public byte[] buildInterested() {
        return buildMessage(Interested);
    }

    public byte[] buildNotInterested() {
        return buildMessage(NotInterested);
    }

    public byte[] buildBitField(boolean[] pieceAvailability) {
        byte[] bytes = Utils.convertBooleanArrayToByteArray(pieceAvailability);
        /*
         TODO
         pad ending bits of byte with 0's
         */
        return buildMessage(BitField, bytes);
    }

    public byte[] buildHave(int pieceIndex) {
        return buildMessage(Have, Utils.intToByteArray(pieceIndex));
    }

    public byte[] buildRequest(int pieceIndex, int begin, int length) {
        ByteBuffer buf = ByteBuffer.allocate(12);
        
        buf.put(Utils.intToByteArray(pieceIndex));
        buf.put(Utils.intToByteArray(begin));
        buf.put(Utils.intToByteArray(length));
        return buildMessage(Request, buf.array());
    }

    public byte[] buildCancel(int pieceIndex, int begin, int length) {
        ByteBuffer buf = ByteBuffer.allocate(12);
        buf.put(Utils.intToByteArray(pieceIndex));
        buf.put(Utils.intToByteArray(begin));
        buf.put(Utils.intToByteArray(length));
        return buildMessage(Cancel, buf.array());
    }

    public byte[] buildPiece(int pieceIndex, int begin, byte[] piece) {
        ByteBuffer buf = ByteBuffer.allocate(8 + piece.length);
        buf.put(Utils.intToByteArray(pieceIndex));
        buf.put(Utils.intToByteArray(begin));
        buf.put(piece);
        return buildMessage(Piece, buf.array());
    }

    public byte[] buildHandShake() throws TorrentException {

//        int capacity =
//            PROTOCOL_HEADER.length() +
//            RESERVED_HEADER_BYTES.length +
//            infoHash.length() +
//            myPeerId.length();
        ByteBuffer buffer = ByteBuffer.allocate(capacity);
        PROTOCOL_HEADER_BYTES.rewind();
        buffer.put((byte) 19); // header
        buffer.put(PROTOCOL_HEADER_BYTES); // header
        buffer.put(RESERVED_HEADER_BYTES); // reserved
        buffer.put(infoHash); // infohash
        buffer.put(Utils.getAsBytes(myPeerId, 20)); // my peerid


        return buffer.array();
    }

    public int getCapacity() {
        return capacity;
    }


    public static boolean isEqual(byte[] base, byte[] target, int startIndex) {
        if (target.length < base.length + startIndex) {
            return false;
        }
        for (int i = 0; i < base.length; i++) {
            if (base[i] != target[i + startIndex]) {
                return false;
            }
        }
        return true;
    }


    public byte[] getInfoHash() {
        return infoHash;
    }


    public String getMyPeerId() {
    	if(myPeerId == null){
    		throw new RuntimeException("try to get myPeerId before it is ser"  );
    	}
        return myPeerId;
    }
    
    public String getConnectingPeerId() {
    	if(connectingPeerID == null){
    		throw new RuntimeException("try to getconnectingPeerID before it is ser"  );
    	}
        return connectingPeerID;
    }
}
